package org.infodavid.common.reflect.impl;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.infodavid.common.annotations.AImplementation;

/**
 * The Class InstantiationUtil.
 */
@SuppressWarnings(value = {
    "unchecked", "rawtypes"
})
public final class InstantiationUtil {

	/** The Constant log. */
	private static final Log LOGGER = LogFactory.getLog(InstantiationUtil.class);

	/** The TESTING flag. */
	private static boolean testing;

	static {
		try {
			testing = Class.forName("junit.framework.Assert") != null;
		}
		catch (final Exception e) {
			testing = false;
		}
	}

	/** The Constant IMPLEMENTATIONS_PROPERTY. */
	private static final String IMPLEMENTATIONS_PROPERTY = "implementations";

	/** The Constant SEPARATOR. */
	public static final String SEPARATOR = ",";

	/**
	 * The Constructor.
	 */
	private InstantiationUtil() {
		super();
	}

	/**
	 * Process.
	 * @param clazz the clazz
	 * @param implementations the implementations
	 */
	public static void processClass(final Class clazz, final Stack<Class> implementations) {
		final AImplementation annotation = (AImplementation)clazz.getAnnotation(AImplementation.class);

		if (testing) {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Testing environment is activated");
			}

			if (annotation == null || annotation.runtime() == AImplementation.ERuntime.DEFAULT) {
				implementations.add(clazz);

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Added with runtime = DEFAULT: " + clazz.getName());
				}
			}
			else if (annotation.runtime() == AImplementation.ERuntime.TEST) {
				implementations.add(0, clazz);

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Added with runtime = TEST: " + clazz.getName());
				}
			}
			else {
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Runtime mode not set for class: " + clazz);
				}
			}
		}
		else {
			if (annotation == null || annotation.runtime() == AImplementation.ERuntime.DEFAULT) {
				implementations.add(0, clazz);

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Added with runtime = DEFAULT: " + clazz.getName());
				}
			}
			else if (annotation.runtime() == AImplementation.ERuntime.TEST) {
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Ignored with runtime = TEST: " + clazz.getName());
				}
			}
			else {
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Runtime mode not set for class: " + clazz);
				}
			}
		}
	}

	/**
	 * Instantiate.
	 * @param <E> the type of the object to instantiate
	 * @param reference the reference
	 * @param implementations the implementations
	 * @return the instance
	 * @throws Exception the exception
	 */
	public static <E> E newInstance(final Class<E> reference, final Stack<Class> implementations)
	    throws Exception {
		Class clazz;

		analyze(implementations);

		clazz = implementations.firstElement();

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Building a new instance of " + clazz + " for reference " + reference);
		}

		final Constructor cons = clazz.getDeclaredConstructor();

		cons.setAccessible(true);

		return (E)cons.newInstance();
	}

	/**
	 * Instantiate.
	 * @param <E> the type of the object to instantiate
	 * @param reference the reference
	 * @param implementations the implementations
	 * @return the instances
	 * @throws Exception the exception
	 */
	public static <E> Collection<E> newInstances(final Class<E> reference,
	    final Stack<Class> implementations) throws Exception {
		final Collection<E> instances = new ArrayList<E>(implementations.size());
		Constructor cons;

		analyze(implementations);

		for (final Class clazz : implementations) {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Building a new instance of " + clazz + " for reference " + reference);
			}

			cons = clazz.getDeclaredConstructor();

			cons.setAccessible(true);
			instances.add((E)cons.newInstance());
		}

		return instances;
	}

	/**
	 * Analyze the inheritance between classes and remove the super classes to keep the derived
	 * classes.
	 * @param classes the classes
	 */
	private static void analyze(final Stack<Class> classes) {
		Class iClass, jClass;
		int i = 0, j = 0;

		while (i < classes.size()) {
			iClass = classes.get(i++);
			j = i;

			while (j < classes.size()) {
				jClass = classes.get(j);

				if (iClass.isAssignableFrom(jClass)) {
					// iClass is a super class of jClass, iClass ignored
					classes.set(--i, jClass);
					classes.remove(j++);
					j = classes.size();
				}
				else if (jClass.isAssignableFrom(iClass)) {
					// jClass is a super class of iClass, jClass ignored
					classes.remove(j++);
				}
				else {
					j++;
				}
			}
		}
	}

	/**
	 * Processing classes.
	 * @param <T> the generic type
	 * @param reference the reference
	 * @param classes the classes
	 * @param implementations the implementations
	 * @throws Exception the exception
	 */
	public static <T> void processClasses(final Class<T> reference, final Set<Class> classes,
	    final Stack<Class> implementations) throws Exception {
		LOGGER.debug("Processing classes...");

		for (final Class clazz : classes) {
			try {
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Processing class: " + clazz);
				}

				if (reference.isAssignableFrom(clazz)) {
					processClass(clazz, implementations);
				}
				else {
					if (LOGGER.isDebugEnabled()) {
						LOGGER.debug("Class: " + clazz.getName() + " is not assignable " + reference.getName());
					}
				}
			}
			catch (final Throwable e) {
				LOGGER.error("Invalid class name: " + clazz, e);
			}
		}
	}

	/**
	 * Analyse.
	 * @param path the path
	 * @param classes the classes
	 * @param loader the loader
	 * @param properties the properties
	 * @throws IOException the IO exception
	 */
	public static void analyseClassPath(final String path, final Collection<Class> classes,
	    final ClassLoader loader, final Properties properties) throws IOException {
		Enumeration<URL> resources = null;
		InputStream in = null;
		String value;

		try {
			resources = loader.getResources(path);
		}
		catch (final Exception e) {
			LOGGER.debug("No properties file found", e);

			resources = null;
		}

		if (resources == null) {
			LOGGER.debug("No properties file found");
		}
		else {
			URL url;

			while (resources.hasMoreElements()) {
				url = resources.nextElement();

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Found descriptor: " + url);
				}

				try {
					in = url.openStream();

					if (in != null) {
						properties.load(in);
						value = properties.getProperty(IMPLEMENTATIONS_PROPERTY);

						if (value == null || value.length() == 0) {
							LOGGER.warn("No implementation defined");
						}
						else {
							final String[] values = value.split(SEPARATOR);

							for (final String name : values) {
								if (LOGGER.isDebugEnabled()) {
									LOGGER.debug("Found definition: " + name);
								}

								if (LOGGER.isDebugEnabled()) {
									LOGGER.debug("Loading class: " + name);
								}

								try {
									classes.add(loader.loadClass(name));
								}
								catch (final Throwable e) {
									LOGGER.error("Invalid class name: " + name, e);
								}
							}
						}
					}
				}
				finally {
					if (in != null) {
						try {
							in.close();
						}
						catch (final Exception e) {
							LOGGER.debug("An error occurs while closing input stream", e);
						}
					}
				}
			}
		}
	}

	/**
	 * Analyse class paths.
	 * @param <T> the generic type
	 * @param reference the reference
	 * @param classes the classes
	 * @param loader the loader
	 * @throws Exception the exception
	 */
	public static <T> void analyseClassPaths(final Class<T> reference, final Set<Class> classes,
	    final ClassLoader loader) throws Exception {
		final String folder = reference.getPackage().getName().replace('.', '/');
		final String filename = reference.getSimpleName() + ".properties";
		final String path = folder + '/' + filename;
		final Properties properties = new Properties();

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Trying to load definitions for class: " + reference + " using class loader: "
			    + loader);
			LOGGER.debug("Trying to load properties from file: " + folder + '/' + filename);
		}

		if (loader != null) {
			try {
				analyseClassPath(path, classes, loader, properties);
			}
			catch (final Exception e) {
				LOGGER.debug("No properties file found in class loader: " + loader, e);
			}
		}

		try {
			analyseClassPath(path, classes, reference.getClassLoader(), properties);
		}
		catch (final Exception e) {
			LOGGER.debug("No properties file found in class loader: " + reference.getClassLoader(), e);
		}

		try {
			analyseClassPath(path, classes, ClassLoader.getSystemClassLoader(), properties);
		}
		catch (final Exception e) {
			LOGGER.debug(
			    "No properties file found in class loader: " + ClassLoader.getSystemClassLoader(), e);
		}
	}
}
